﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using Selenium;

namespace Quail
{
    /// <summary>
    /// Implements access to the web browser through Selenium calls
    /// </summary>
    public class WebBrowser : SimpleBrowser
    {
        private NetworkTraffic _traffic;
        private SeleniumLogLevel _logLevel;
        private int _setRunSpeed;
		private readonly Dictionary<string, string> _evalFunctionDict = new Dictionary<string, string>();
        
        /// <summary>
        /// The log level for the selenium window
        /// </summary>
        public enum SeleniumLogLevel
        {
            debug,
            info,
            warn,
            error,
            off
        }

        /// <summary>
        /// Initializes a new instance of the WebBrowser class (this does NOT create a new browser window!)
        /// </summary>
        /// <param name="ts">The test session</param>
        public WebBrowser(TestSession ts)
            : this(ts, null, null)
        {
        }

        /// <summary>
        /// Initializes a new instance of the WebBrowser class  (this does NOT create a new browser window!)
        /// </summary>
        /// <param name="ts">The test session</param>
        /// <param name="page">The web page class</param>
        public WebBrowser(TestSession ts, WebPage page)
            : this(ts, page, null)
        {
        }

        /// <summary>
        /// Initializes a new instance of the WebBrowser class  (this does NOT create a new browser window!)
        /// </summary>
        /// <param name="ts">The test session</param>
        /// <param name="page">The web page class</param>
        /// <param name="cookie">The cookie object</param>
        public WebBrowser(TestSession ts, WebPage page, WebCookie cookie)
            :base(ts, null, null)
        {
            GenericPage = page ?? new WebPage(ts);
            Cookie = cookie ?? new WebCookie(this);

        	ts.OnBeforeStartBrowser += OnBrowserStarting;
        }

    	/// <summary>
        /// Gets the default Generic web page
        /// </summary>
        public new WebPage GenericPage
        {
            get { return base.GenericPage as WebPage; }
            set { base.GenericPage = value; }
        }

        /// <summary>
        /// Gets the current Selenium interface
        /// </summary>
        public override ISelenium SeleniumInterface
        {
            get
            {
                return SeleniumTestSession.SeleniumInterface;
            }
        }

        /// <summary>
        /// Gets the current test session
        /// </summary>
        public SeleniumTestSession SeleniumTestSession
        {
            get { return (SeleniumTestSession) TestSession; }
        }

        /// <summary>
        /// Gets the number of milliseconds for timeouts as a string
        /// </summary>
        protected override void SetTimeout(int timeout)
        {
            base.SetTimeout(timeout);
            SeleniumInterface.SetTimeout(Timeout.ToString());
        }

        /// <summary>
        /// Gets or sets the selenium run speed in milliseconds
        /// </summary>
        public override int RunSpeed
        {
            get
            {
                return int.Parse(SeleniumInterface.GetSpeed());
            }

            set
            {
                _setRunSpeed = value;
                SeleniumInterface.SetSpeed(value.ToString());
            }
        }

        /// <summary>
        /// Gets the network traffic logger
        /// </summary>
        public override NetworkTraffic NetworkTrafficLogger
        {
            get
            {
                if (!SeleniumTestSession.CaptureNetworkTraffic)
                    throw new NotSupportedException("You must enable CaptureNetworkTraffic on the test session in order to get network traffic statistics");

                if (_traffic == null)
                    _traffic = new NetworkTraffic(this);
                return _traffic;
            }
        }

        /// <summary>
        /// Gets or sets the Selenium log level.  To view selenium logs, click 'view logs' from the selenium browser window.
        /// </summary>
        public SeleniumLogLevel LogLevel
        {
            get
            {
                return _logLevel;
            }

            set
            {
                SeleniumInterface.SetBrowserLogLevel(value.ToString());
                _logLevel = value;
            }
        }

    	private string _location;
    	private DateTime? _locationExpireTime;

		/// <summary>
		/// Gets the location of the selenium browser.  The result is cached for 500 milliseconds
		/// </summary>
		/// <returns>The current browser location</returns>
        protected override string GetLocation()
        {
        	var now = DateTime.UtcNow;

			if (_locationExpireTime.HasValue && _locationExpireTime.Value > now)
				return _location;

            _location = DoQuick(() => DoCommandNoLogging("getLocationWithoutStatusReset"));
        	_locationExpireTime = now.AddMilliseconds(500);

        	return _location;
        }

        /// <summary>
        /// Retrieves the User Agent String of the current browser instance via Javascript call.
        /// </summary>
        /// <returns>User Agent string</returns>
        public string GetUserAgent()
        {
            return this.SeleniumInterface.GetEval("navigator.userAgent");
        }

        private ICommandProcessor Processor
        {
            get
            {
                return SeleniumTestSession.Processor;
            }
        }

		/// <summary>
		/// Waits for javascript to return true within the specified timeout
		/// </summary>
		/// <param name="javascript">The javascript to execute (to access selenium commands use:  "selenium.X".  See ISeleniumInterface.WaitForCondition</param>
		/// <param name="waitingForName">What it is you're waiting for</param>
		/// <param name="timeout">The timeout to wait (default: Timeout)</param>
		/// <param name="message">The message to use if the javascript does not return true within the timeout</param>
		/// <param name="rollbackTaskOnSuccess">Set this flag to true to rollback a successful task for cleaner logging</param>
		public void WaitForJavascriptToReturnTrue(
			string javascript, 
			string waitingForName, 
			int? timeout = null,
			string message = null,
			bool rollbackTaskOnSuccess = false)
		{
			if (!timeout.HasValue)
				timeout = Timeout;

			int restoreTimeout = SeleniumTestSession.Processor.DefaultHttpTimeout;

			var task = ActiveTask.BeginTask("WaitFor:" + waitingForName);
			try
			{
				if (restoreTimeout < timeout.Value + 1000)
					SeleniumTestSession.Processor.DefaultHttpTimeout = timeout.Value + 1000;

				SeleniumInterface.WaitForCondition(GetCachedJavascriptFn(javascript, "selenium"), timeout.Value.ToString(CultureInfo.InvariantCulture));
				if (rollbackTaskOnSuccess)
					task.Rollback();
				else
					task.Finish();
			}
			catch (Exception ex)
			{
				if (ex.Message.Contains("Timed out after"))
				{
					message = message ?? string.Format("Timed out waiting for {0} javascript to return true:\n{1}", waitingForName, javascript);
					task.Finish(LogStatus.FailTimeout, message);
					throw new WaitParameters.WaitTimeoutException(LogStatus.FailTimeout, message);
				}

				task.Finish(ex);
				throw;
			}
			finally
			{
				SeleniumTestSession.Processor.DefaultHttpTimeout = restoreTimeout;
			}
		}


        /// <summary>
        /// Puts focus on browser window
        /// </summary>
        public void Focus()
        {
            var task = ActiveTask.BeginTask("WebBrowser.Focus");
            try
            {
                SeleniumInterface.WindowFocus();
                task.Finish();
            }
            catch (Exception e)
            {
                task.Finish(e);
                throw;
            }
        }

        /// <summary>
        /// Gets all the window names
        /// </summary>
        /// <returns>An array of browser window names</returns>
        public string[] GetAllWindowNames()
        {
            return SeleniumInterface.GetAllWindowNames();
        }

        /// <summary>
        /// Selects a popup by window name.  Use DeselectPopup to remove the popup selection.
        /// </summary>
        /// <param name="name">the window name</param>
        public void SelectPopup(string name)
        {
            SeleniumInterface.SelectPopUp(name);
        }

        /// <summary>
        /// Deselects a popup
        /// </summary>
        public void DeselectPopup()
        {
            SeleniumInterface.DeselectPopUp();
        }


        /// <summary>
        /// Called by Back() method to back the web page
        /// </summary>
        protected override void PerformBack()
        {
            SeleniumInterface.GoBack();
        }

        /// <summary>
        /// Navigates to the web page
        /// </summary>
        /// <param name="url">the url to open</param>
        protected override void PerformNavigate(string url)
        {
            url = ModifyUrlBeforeNavigation(url);
            SeleniumInterface.GetEval("window.location.href = '" + url + "'");
			ClearLocationCache();
        }

		/// <summary>
		/// Modifies the url before navigation
		/// </summary>
		/// <param name="url"></param>
		/// <returns></returns>
        public String ModifyUrlBeforeNavigation(String url)
        {
            if (GlobalUrlModification != null)
                return GlobalUrlModification(url);
            return url;
        }

        /// <summary>
        /// Refreshes the web page
        /// </summary>
        protected override void PerformRefresh()
        {
            SeleniumInterface.Refresh();
        }

        /// <summary>
        /// Executes a Selenium command
        /// </summary>
        /// <param name="command">The command to execute</param>
        /// <param name="parms">Parameters for the command</param>
        /// <returns>The result of the command</returns>
        public string DoCommand(string command, params string[] parms)
        {
            try
            {
                var result = DoCommandNoLogging(command, parms);

                return result;
            }
            catch (Exception e)
            {
                var task = ActiveTask.BeginTask("Exec " + command);
                task.Finish(e);
                throw;
            }
        }

        /// <summary>
        /// Executes a Selenium command without logging it to the Logger object
        /// </summary>
        /// <param name="command">The command to execute</param>
        /// <param name="parms">Parameters for the command</param>
        /// <returns>The result of the command</returns>
        public string DoCommandNoLogging(string command, params string[] parms)
        {
            var result = Processor.DoCommand(command, parms);

            return result.StartsWith("OK,") ? result.Substring(3) : string.Empty;
        }

        /// <summary>
        /// Executes a Selenium command that returns a string array
        /// </summary>
        /// <param name="command">The command to execute</param>
        /// <param name="parms">Parameters for the command</param>
        /// <returns>The result of the command</returns>
        public string[] DoCommandToStringArray(string command, params string[] parms)
        {
            var task = ActiveTask.BeginTask("Exec " + command);
            try
            {
                var result = Processor.GetStringArray(command, parms);

                task.Rollback();

                return result;
            }
            catch (Exception e)
            {
                task.Finish(e);
                throw;
            }
        }

		/// <summary>
		/// Evaluates javascript against the browser
		/// </summary>
		/// <param name="eval">Javascript command to evaluate</param>
		/// <returns>The result of the javascript command</returns>
		public string GetEval(string eval)
		{
			return GetEval(eval, false);
		}

        /// <summary>
        /// Evaluates javascript against the browser
        /// </summary>
        /// <param name="eval">Javascript command to evaluate</param>
		/// <param name="cacheEval">Set to true to enable caching the eval text for better performance</param>
        /// <returns>The result of the javascript command</returns>
        public string GetEval(string eval, bool cacheEval)
        {
            try
            {
            	var toEval = cacheEval ? GetCachedJavascriptFn(eval, "this") : eval;

            	var result = SeleniumInterface.GetEval(toEval);
            	return result;
            }
            catch (Exception e)
            {
                var task = ActiveTask.BeginTask(string.Format("GetEval({0})", eval));
                task.Finish(e);
                throw;
            }
        }

		/// <summary>
		/// Caches javascript in the selenium session to avoid network traffic overhead
		/// </summary>
		/// <param name="javascript">The javascript to cache</param>
		/// <param name="seleniumName">The name of selenium object in javascript (usually 'this' or 'selenium')</param>
		/// <returns>The new javascript to execute (the name of the cached method)</returns>
		public string GetCachedJavascriptFn(string javascript, string seleniumName)
		{
			string quickName;
			var key = string.Format("{0}|{1}", javascript, seleniumName);

			if (!_evalFunctionDict.TryGetValue(key, out quickName))
			{
				quickName = "Z" + _evalFunctionDict.Count;
				SeleniumInterface.GetEval(string.Format("this.{0}=function(){{return {1}}}",
				                                        quickName,
				                                        javascript));

				quickName = string.Format("{0}.{1}()", seleniumName, quickName);
				_evalFunctionDict.Add(key, quickName);
			}

			return quickName;
		}

		/// <summary>
		/// Clears the javascript function names cached with GetCachedJavascript
		/// </summary>
		public void ClearCachedJavascript()
		{
			_evalFunctionDict.Clear();
		}

        /// <summary>
        /// Executes the action without delay from Selenium
        /// </summary>
        /// <param name="whatever">The action to perform</param>
        public void DoQuick(Action whatever)
        {
            try
            {
                //if (_setRunSpeed != 0)
                //   SeleniumInterface.SetSpeed("0");

                whatever();

            }
            finally
            {
                //if (_setRunSpeed != 0)
                //    SeleniumInterface.SetSpeed(_setRunSpeed.ToString());
            }
        }

        /// <summary>
        /// Executes the action without delay from Selenium
        /// </summary>
        /// <param name="whatever">The action to perform</param>
        public TResult DoQuick<TResult>(Func<TResult> whatever)
        {
            try
            {
                //if (_setRunSpeed != 0)
                //    SeleniumInterface.SetSpeed("0");

                return whatever();
            }
            finally
            {
                //if (_setRunSpeed != 0)
                //   SeleniumInterface.SetSpeed(_setRunSpeed.ToString());
            }
        }

        protected override string PerformGetUrlContent(string url)
        {
            return DoCommand("getContentFromUrl", url);
        }

		/// <summary>
		/// Clears the GetLocation() cache
		/// </summary>
    	public void ClearLocationCache()
    	{
    		_location = null;
    		_locationExpireTime = null;
    	}

		private void OnBrowserStarting(object sender, TestSession.StartBrowserEventArgs e)
		{
			ClearCachedJavascript();
		}
    }
}
